/**
 * 
 */
package server.connection;

import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.log4j.Logger;

import server.conf.Preferences;
import server.connection.listening.MainSelectorThread;
import server.connection.listening.SelectorThread;
import server.connection.listening.SocketChannelDTO;
import server.connection.pool.SelectorThreadPool;
import server.exception.ServerException;
import server.log.Logging;

/**
 * Module that manages all working threads.
 * It executes new selector threads, and new connections are added via this module. 
 * 
 * Module is implemented as a standalone SelectorAdapter object. 
 * 
 * @author Kamil Nowak
 *
 */
public class SelectorAdapter {

	private Logger logger = Logging.instance().getLogger(SelectorAdapter.class.toString());
	
	private int nThreads = Integer.valueOf(Preferences.instance().getPreference("default_threads_number"));
	private int maxThreads = Integer.valueOf(Preferences.instance().getPreference("max_threads_number"));
	private long keepAliveTime = Integer.valueOf(Preferences.instance().getPreference("keep_alive_time"));
	private TimeUnit DEFAULT_TIMEUNIT = TimeUnit.MILLISECONDS;
		
	private int connectionsPerThread = Integer.valueOf(Preferences.instance().getPreference("connections_per_thread"));
	
	private int defaultConnsNum = nThreads * connectionsPerThread;
	private int maxConnsNum = nThreads * maxThreads;
	
	private final static SelectorAdapter instance = new SelectorAdapter();
	

	private SelectorThreadPool selectorThreadPool = new SelectorThreadPool(nThreads,
			maxThreads,
			keepAliveTime,
			DEFAULT_TIMEUNIT,
			new SynchronousQueue<Runnable>());  

	/**
	 * unique id assigned to each selectorThread
	 */
	private AtomicLong id = new AtomicLong(0);
	
	/**
	 * thread which listenes for incoming connection
	 */
	private MainSelectorThread mainSelector = new MainSelectorThread();
	
	/**
	 * keeps threads being currently executed 
	 */
	private final List<SelectorThread> threads = Collections.synchronizedList( new LinkedList<SelectorThread>() ); 
	
	/**
	 * keeps connection and thread mapping
	 */
	private final ConcurrentHashMap<Long,SelectorThread> connection2Thread = new ConcurrentHashMap<Long,SelectorThread>(defaultConnsNum);
	
	private SelectorAdapter(){
		init();
	}
	
	private void init() {
		for (int i = 0; i < nThreads; i++)
			try {
				SelectorThread st = new SelectorThread(id.incrementAndGet(),false);
				threads.add(st);
				selectorThreadPool.execute(st);
			} catch (ServerException e) {
				logger.error("Error while creating selectorThreads",e);
			}
	}

	/**
	 * Gets standalone instance of SelectorAdapter class
	 * @return standalone object
	 */
	public static SelectorAdapter instance() {
		return instance;
	}
	
	/**
	 * Starts main selector, thread that listens to incoming connection
	 */
	public void startMainSelectorThread() {
		new Thread(mainSelector).start();
	}

	/**
	 * @return configured connections per thread number
	 */
	public int getConnectionsPerThread() {
		return connectionsPerThread;
	}
	
	/**
	 * Add new socketChannel by MainSelectorThread. 
	 * Connection is inserted to selectorThread with the lowest number of servised connection
	 * 
	 * @param socketChannelDTO
	 * @throws ServerException
	 */
	public void addSocketChannel(SocketChannelDTO socketChannelDTO) throws ServerException {
		
		logger.info("Adding SocketChannel: " + socketChannelDTO.toString());
		SelectorThread selectorThread;
		do {
			selectorThread = findNotFullSelectorThread();
		
			if (selectorThread == null) {
				logger.debug("Creating new SelectorThread");
				selectorThread = new SelectorThread(id.incrementAndGet(),true);
				threads.add(selectorThread);
				selectorThreadPool.execute( selectorThread );
			}
		
		
		} while(!selectorThread.addSocketChannel(socketChannelDTO));
		
		connection2Thread.put( socketChannelDTO.getConnectionID(), selectorThread );
		selectorThread.wakeup();
	}
	
	/**
	 * @return first not full selectionThread in the threads list
	 * TODO Implement better algorithm for search and balance selectionThreads - Heap?
	 */
	private SelectorThread findNotFullSelectorThread() {
		logger.info("Seraching not full SelectorThread");
		int minConnections = Integer.MAX_VALUE;
		SelectorThread minSt = null;
		
		for (Iterator<SelectorThread> i = threads.iterator(); i.hasNext(); ) {
			SelectorThread st = i.next();
			if (!st.isOperating()) { i.remove(); continue; }
			if (st.getSocketsNumber() < minConnections) {
				minSt = st;
				minConnections = st.getSocketsNumber();
				//return st;
			}
		}
		
		logger.debug("Seraching not full SelectorThread has not been found");	
		return minConnections >= connectionsPerThread ? null : minSt;
	}
	
	/**
	 * Gets selectorThread that manages specified connectionId 
	 * 
	 * @param connectionId connection id to find
	 * @return found selector thread or null
	 */
	public SelectorThread getSelectorThread(Long connectionId) {
		return connection2Thread.get( connectionId ); 
	}

	/**
	 * Cleans all connection Id data from the module
	 * 
	 * @param connectionId connection to clean
	 */
	public void removeConnection(Long connectionId) {
		connection2Thread.remove(connectionId);
	}

	/**
	 * @return configured connections number per thread
	 */
	public int getDefaultConnsNum() {
		return defaultConnsNum;
	}

	/**
	 * @return configured max connections number per thread
	 */
	public int getMaxConnsNum() {
		return maxConnsNum;
	}
	
	
	
}
